/*
This file is part of MMM.

MMM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

MMM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with MMM.  If not, see <http://www.gnu.org/licenses/>.
*
* @package    MMM
* @author     Andre Meixner
* @copyright  2017 High Performance Humanoid Technologies (H2T), Karlsruhe, Germany
*
*/

#ifndef __MMM_FACTORYPLUGINLOADER_H_
#define __MMM_FACTORYPLUGINLOADER_H_

#include "MMMCore.h"
#include "MMMImportExport.h"
#include "AbstractFactoryMethod.h"

#include <boost/extension/shared_library.hpp>
#include <boost/foreach.hpp>
#include <filesystem>
#include <boost/function.hpp>
#include <iostream>
#include <string>
#include <set>

#ifdef WIN32
#define LIB_EXTENSION ".dll"
#else
#define LIB_EXTENSION ".so"
#endif

namespace MMM
{

template <typename Factory>
/*! \brief A templated class for easy plugin loading from libraries with unique ids. */
class MMM_IMPORT_EXPORT FactoryPluginLoader
{
public:
    FactoryPluginLoader(const std::vector<std::string> &libPaths) {
        addPluginLibs(libPaths);
    }

    FactoryPluginLoader(const std::string &librarySearchPath) {
        addPluginLibs(librarySearchPath);
    }

    FactoryPluginLoader() {
    }

    int addPluginLibs(const std::vector<std::string> &libPaths) {
        int addedLibs = 0;
        for (const std::string &librarySearchPath : libPaths) {
            addedLibs += addPluginLibs(librarySearchPath);
        }
        return addedLibs;
    }

    int addPluginLibs(const std::string &librarySearchPath) {
        int addedLibs = 0;
        try {
            std::filesystem::path targetDir(librarySearchPath);
            if (!is_directory(targetDir)) {
                MMM_INFO << "Path '" <<  librarySearchPath << "' does not exist or is not a directory!" << std::endl;
            }
            else {
                std::filesystem::directory_iterator it(targetDir), eod;
                BOOST_FOREACH(std::filesystem::path const &p, std::make_pair(it, eod)) {
                    if (p.extension().string() == LIB_EXTENSION && is_regular_file(p)) {
                        boost::extensions::shared_library lib(p.string());
                        if (lib.open()) {
                            MMM_INFO << p.string() << " opened as boost shared lib.\n";
                            boost::function<boost::shared_ptr<Factory>()> getFactory = lib.get<boost::shared_ptr<Factory>>("getFactory");
                            boost::function<std::string()> getVersion = lib.get<std::string>("getVersion");
                            if (getFactory) {
                                boost::shared_ptr<Factory> factory = getFactory();
                                if (factory->checkBase()) {
                                    if (getVersion) {
                                        std::string version = getVersion();
                                        if (getVersion() == Factory::VERSION) {
                                            factory->setPath(p.string());
                                            addedLibs += addFactory(factory);
                                        }
                                        else MMM_ERROR << "Loaded plugin " << factory->getName() << " from path " << p.string() << " has version " << version << ", which doesn't match the interface with version " << Factory::VERSION << std::endl;
                                    }
                                    else MMM_ERROR << "Loaded plugin " << factory->getName() << " from path " << p.string() << " needs to have a getVersion()-Method." << std::endl;
                                }
                                else MMM_ERROR << "Loaded plugin " << factory->getName() << " is of wrong Base type." << std::endl;
                            }
                            else MMM_ERROR << "Loaded plugin from path " << p.string() << " needs to have a getFactory()-Method." << std::endl;
                        } else {
                            MMM_ERROR << p.string() << " could not be opened as boost shared lib.\n";
                            MMM_ERROR << "The error was: " << dlerror() << std::endl;
                        }
                    }
                }
            }
        } catch (const std::filesystem::filesystem_error& exc) {
            MMM_INFO << "Path '" <<  librarySearchPath << "' could not be opened!" << std::endl;
        }
        return addedLibs;
    }

    boost::shared_ptr<Factory> loadFactory(const std::string &id, const std::string &version = std::string()) {
        if (factories.find(id + version) != factories.end()) {
            return factories[version.empty() ? id : id + "_v" + version];
        } else {
            std::string versionString = std::string();
            if (!version.empty()) versionString = "' and version '" + version;
            MMM_ERROR << "Cannot find plugin with id '" << id << versionString << "'" << std::endl;
            return nullptr;
        }
    }

    std::map<std::string, boost::shared_ptr<Factory> > getFactories() {
        return factories;
    }

protected:
    virtual int addFactory(boost::shared_ptr<Factory> factory) {
        if (factories.find(factory->getName()) == factories.end()) {
            factories[factory->getName()] = factory;
            return 1;
        }
        else {
            MMM_ERROR << "Duplicate plugin of " << factory->getName() << " found. Ignoring second one." << std::endl;
            return 0;
        }
    }

    std::map<std::string, boost::shared_ptr<Factory> > factories;

};

}
#endif // __MMM_FACTORYPLUGINLOADER_
